今天不學新語法。我準備了一個藏了 4 個 bug 的遊戲版本,然後用我本業的方式把它們找出來。
規則:先不看程式碼,像測產品那樣操作,再用 DevTools 追原因。
這是我這 21 天一直在遇到、但今天才正式整理的東西:
| 大聲失敗 | 安靜失敗 | |
|---|---|---|
| 例子 | 404、SyntaxError、TypeError | 功能沒生效、算錯、畫面沒更新 |
| 使用者看得到嗎 | 看得到(畫面壞掉、跳錯誤) | 看不到,一切正常 |
| 誰會回報 | 所有人 | 幾乎沒有人 |
| 好不好查 | 訊息會告訴你檔名和行號 | 沒有任何線索 |
這 21 天累積下來的安靜失敗清單:字串改一個字不生效、同名函式安靜覆蓋、遮蔽留下的 undefined、監聽器掛上 undefined、資料改了畫面沒動、使用者輸入的標籤被解析、忘記清除的計時器在背景永遠跑、亂數少了 +1、物件存進 localStorage 變成 [object Object]。
九個裡面只有一個會報錯。
而今天這四個 bug 的比例也差不多:一個大聲,三個安靜。
第一輪,我完全不看程式碼,只當使用者:
進行遊戲測試
無法執行清除記錄
出現訊息 Uncaught ReferenceError: showRecrod is not defined
點擊再戰一次
血量都恢復了,但點擊攻擊後沒有扣血,顯示「請不要對小怪屍體動手!」
切換視窗後
遊戲頁面被清除記錄了
第二輪才打開 DevTools 追原因。這個順序是刻意的:先確認「現象」,再找「原因」。如果一開始就盯著程式碼看,很容易被自己的假設帶著走,反而漏掉使用者真正會遇到的問題。
點下去,紀錄文字沒變,主控台跳紅字:
Uncaught ReferenceError: showRecrod is not defined
函式名稱打錯了,showRecord 寫成 showRecrod。
這個最好找,因為它會自己舉手。但它有個細節值得注意:紀錄其實真的被刪掉了。因為 removeItem 那行在前面,已經執行了;錯誤發生在下一行,只是讓畫面沒更新。
所以我後來「切換視窗回來發現紀錄不見了」,並不是第三個 bug —— 那是同一個 bug 的延遲現身:資料早就沒了,只是畫面還在騙我。
追這個 bug 的時候,我發現 DevTools 有一個選項叫「在遇到未偵測到的例外狀況時暫停」。打開它之後,程式一出錯就會自動停在那一行,右邊直接顯示例外狀況的內容,不用自己去猜。

這個是我最後才找到的。
打一場,慢慢打,盯著小怪的 HP 數字什麼時候變紅 —— 30 左右就變了。
但小怪滿血 60,設計上是「剩 30% 變紅」,也就是 18 才對。
原因是判斷式裡用錯了物件:
if (state.enemy.hp <= state.hero.maxHp * 0.3)
// ^^^^ 應該是 enemy.maxHp
hero.maxHp 是 100,所以門檻變成 30。
這是複製貼上最典型的後遺症:型別對、語法對、程式跑得很順,只是拿錯了物件。完全不會報錯。
「最少回合紀錄」的比較方向寫反了:
if (best === null || turns > best) { // 應該是 turns < best
變成「花更多回合才算破紀錄」。
這個 bug 我一開始完全測不出來,原因有兩層。
第一層:我需要打贏好幾場才能比較,但 Bug 4 擋住了我 —— 打完一場按「再戰一次」,遊戲就壞掉了,根本進不了第二場。
第二層更麻煩:我用 F5 繞過之後連打三場,結果是 4、4、4。
以目前的攻擊力,每場都剛好 4 回合。根本沒有變化可以觀察。 這本身也是個發現:傷害浮動 ±3 太小了,對「最少回合挑戰」這個設計來說是個問題 —— 如果每個人都是 4 回合,那還挑戰什麼?
最後我改用另一個方法:直接在 DevTools 的「應用程式」分頁,把紀錄手動改成 6。
然後打贏一場(4 回合)。正確的行為應該是紀錄更新成 4 —— 結果它還是 6。
抓到了。
這個方法本身值得記下來:不用真的製造出測試條件,直接把系統放進那個狀態就好。我昨天才學到 localStorage 可以被手動修改,今天就拿它來當測試工具。
打贏 → 按「再戰一次」→ 血量回滿、按鈕變回「攻擊!」→ 按下去卻顯示「請不要對小怪屍體動手!」
原因是 reset() 忘記重置 state.isOver。
令我意外的是:點擊再戰一次完全沒有出現任何錯誤。
這句話是我測試當下寫在筆記裡的,後來想想,「意外」這個反應本身就說明了問題:我下意識期待壞掉的東西會報錯。但安靜失敗就是不會 —— 畫面顯示得好好的,狀態卻是錯的。

追這個 bug 的時候我用了中斷點:在程式碼的行號上點一下,程式跑到那裡就會停住,右邊的「範圍」面板會列出當下每個變數的值。
比起 console.log,中斷點的好處是不用改程式碼,而且一次看到全部變數。console.log 適合「我猜是這個變數,印出來看看」;中斷點適合「我不知道是哪個變數不對」。
對我來說,最難的是 Bug 3(紀錄比較寫反)。因為以現在的攻擊力,贏的回合數都落在 4,根本看不出有問題 —— 我得先繞過另一個 bug,再自己造條件,才測得到。
但對一般玩家來說,最難的應該是 Bug 2(血量變紅時機)。
理由是:這個遊戲沒有說明文字。玩家根本不知道「應該剩 30% 才變紅」這個規則,所以不會覺得有問題。
寫下這句話的時候我愣了一下,因為它其實是一個蠻核心的道理:
一個 bug 要能被「發現」,前提是有人知道「正確的行為應該是什麼」。
沒有規格、沒有說明,使用者就算天天看到那個畫面,也永遠不會回報。他們不是沒注意到,是沒有基準可以比對。
這解釋了我工作上很多事:為什麼測試要有規格文件、為什麼「使用者沒反應」不等於「沒問題」、為什麼有些 bug 上線好幾年才被發現 —— 通常是因為某天終於有人問了一句「這裡本來應該是這樣嗎?」
Bug 4 擋住 Bug 3 這件事,是今天最真實的一段。
在真實的測試裡這很常見。我自己就遇過:一個版面的主要功能是壞的,所以掛在上面的新功能根本沒辦法測,只能等主要功能修好。
處理方式有兩種:等修好,或想辦法繞過。今天我用 F5 繞過(重新整理會重置 isOver,但 localStorage 的紀錄會保留),沒有停在那裡等。
而繞過之後才發現,原來後面還有一個 bug 在等我。如果當時就停下來回報「再戰一次壞了」,Bug 3 到今天都不會有人知道。
今天用到的除錯工具,各有適合的場合:
| 工具 | 適合什麼時候 |
|---|---|
console.log |
我猜是這個變數,印出來確認 |
| 中斷點 | 我不知道哪裡不對,想停下來看全部 |
| 例外狀況自動暫停 | 有紅字,想直接跳到出錯的那一行 |
另外兩個我今天才知道的小工具:
console.table(陣列); // 把資料印成表格,看清單時超好用
console.count("標記"); // 自動計數,不用自己加變數
今天搞懂的事:
console.log 適合「我猜是這個」另外意外發現一個設計問題:傷害浮動 ±3 太小,每場都是 4 回合結束。「最少回合挑戰」需要更大的變化才有意義 —— 這件事留到後面組裝時處理。
明天做第二階段回顧,然後開始把這 22 天的零件組裝成真正的作品。